/*
 * This file is part of aion-unique <aion-unique.smfnew.com>.
 *
 *  aion-unique is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-unique is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-unique.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionemu.gameserver.utils.stats;

import org.apache.log4j.Logger;

import com.aionemu.commons.utils.Rnd;
import com.aionemu.gameserver.configs.main.FallDamageConfig;
import com.aionemu.gameserver.controllers.attack.AttackStatus;
import com.aionemu.gameserver.model.SkillElement;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.Item;
import com.aionemu.gameserver.model.gameobjects.Npc;
import com.aionemu.gameserver.model.gameobjects.player.Equipment;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.gameobjects.state.CreatureState;
import com.aionemu.gameserver.model.gameobjects.stats.CreatureGameStats;
import com.aionemu.gameserver.model.gameobjects.stats.PlayerGameStats;
import com.aionemu.gameserver.model.gameobjects.stats.StatEnum;
import com.aionemu.gameserver.model.templates.item.EAttackType;
import com.aionemu.gameserver.model.templates.item.WeaponStats;
import com.aionemu.gameserver.model.templates.item.WeaponType;
import com.aionemu.gameserver.model.templates.npcTemplate.CreatureTemplate;
import com.aionemu.gameserver.network.aion.serverpackets.SM_ATTACK_STATUS;
import com.aionemu.gameserver.skillengine.effect.DamageEffect;
import com.aionemu.gameserver.skillengine.effect.EffectTemplate;
import com.aionemu.gameserver.utils.PacketSendUtility;

/**
 * @author ATracer
 * @author alexa026
 */
public class StatFunctions
{
	private static Logger	log	= Logger.getLogger(StatFunctions.class);

	/**
	 * 
	 * @param player
	 * @param target
	 * @return XP reward from target
	 */
	public static long calculateSoloExperienceReward(Player player, Npc target)
	{
		int playerLevel = player.getObjectTemplate().getLevel();
		int targetLevel = target.getLevel();

		int baseXP = target.getObjectTemplate().getCreatureStatData().getExp();
		int xpPercentage = XPRewardEnum.xpRewardFrom(targetLevel - playerLevel);

		return (int) Math.floor(baseXP * xpPercentage * player.getRates().getXpRate() / 100);
	}

	/**
	 * 
	 * @param player
	 * @param target
	 * @return
	 */
	public static long calculateGroupExperienceReward(int maxLevelInRange, Npc target)
	{
		int targetLevel = target.getLevel();

		int baseXP = target.getObjectTemplate().getCreatureStatData().getExp();
		int xpPercentage = XPRewardEnum.xpRewardFrom(targetLevel - maxLevelInRange);

		return (int) Math.floor(baseXP * xpPercentage / 100);
	}

	/**
	 * ref: http://www.aionsource.com/forum/mechanic-analysis/42597-character-stats-xp-dp-origin-gerbator-team-july-2009-a.html
	 * 
	 * @param player
	 * @param target
	 * @return DP reward from target
	 */

	public static int calculateSoloDPReward(Player player, Npc target)
	{
		int playerLevel = player.getObjectTemplate().getLevel();
		int targetLevel = target.getLevel();
		int baseDP = (int) (target.getObjectTemplate().getCreatureStatData().getExp() * 0.04);

		int xpPercentage = XPRewardEnum.xpRewardFrom(targetLevel - playerLevel);
		return (int) Math.floor(baseDP * xpPercentage * player.getRates().getXpRate() / 100);

	}

	/**
	 * 
	 * @param player
	 * @param target
	 * @return AP reward
	 */
	public static int calculateSoloAPReward(Player player, Creature target)
	{
		int playerLevel = player.getObjectTemplate().getLevel();
		int targetLevel = target.getLevel();
		int percentage = XPRewardEnum.xpRewardFrom(targetLevel - playerLevel);
		return (int) Math.floor(10 * percentage * player.getRates().getApNpcRate() / 100);
	}

	/**
	 * 
	 * @param maxLevelInRange
	 * @param target
	 * @return
	 */
	public static int calculateGroupAPReward(int maxLevelInRange, Creature target)
	{
		int targetLevel = target.getLevel();
		int baseAP = 10;

		int apPercentage = XPRewardEnum.xpRewardFrom(targetLevel - maxLevelInRange);

		return (int) Math.floor(baseAP * apPercentage / 100);
	}

	/**
	 * 
	 * @param defeated
	 * @param winner
	 * @return Points Lost in PvP Death
	 */
	public static int calculatePvPApLost(Player defeated, Player winner)
	{
		int pointsLost = Math.round(defeated.getAbyssRank().getRank().getPointsLost() * defeated.getRates().getApPlayerRate());

		// Level penalty calculation
		int difference = winner.getLevel() - defeated.getLevel();

		if (difference > 4)
		{
			pointsLost = Math.round(pointsLost * 0.1f);
		}
		else
		{
			switch (difference)
			{
				case 3:
					pointsLost = Math.round(pointsLost * 0.85f);
					break;
				case 4:
					pointsLost = Math.round(pointsLost * 0.65f);
					break;
			}
		}
		return pointsLost;
	}

	/**
	 * 
	 * @param defeated
	 * @param winner
	 * @return Points Gained in PvP Kill
	 */
	public static int calculatePvpApGained(Player defeated, int maxRank, int maxLevel)
	{
		int pointsGained = Math.round(defeated.getAbyssRank().getRank().getPointsGained());

		// Level penalty calculation
		int difference = maxLevel - defeated.getLevel();

		if (difference > 4)
		{
			pointsGained = Math.round(pointsGained * 0.1f);
		}
		else if (difference < -3)
		{
			pointsGained = Math.round(pointsGained * 1.3f);
		}
		else
		{
			switch (difference)
			{
				case 3:
					pointsGained = Math.round(pointsGained * 0.85f);
					break;
				case 4:
					pointsGained = Math.round(pointsGained * 0.65f);
					break;
				case -2:
					pointsGained = Math.round(pointsGained * 1.1f);
					break;
				case -3:
					pointsGained = Math.round(pointsGained * 1.2f);
					break;
			}
		}

		// Abyss rank penalty calculation
		int winnerAbyssRank = maxRank;
		int defeatedAbyssRank = defeated.getAbyssRank().getRank().getId();
		int abyssRankDifference = winnerAbyssRank - defeatedAbyssRank;

		if (winnerAbyssRank <= 7 && abyssRankDifference > 0)
		{
			float penaltyPercent = abyssRankDifference * 0.05f;

			pointsGained -= Math.round(pointsGained * penaltyPercent);
		}

		return pointsGained;
	}

	/**
	 * 
	 * @param player
	 * @param target
	 * @return DP reward
	 */
	public static int calculateGroupDPReward(Player player, Npc target)
	{
		int playerLevel = player.getObjectTemplate().getLevel();
		int targetLevel = target.getLevel();

		int xpPercentage = XPRewardEnum.xpRewardFrom(targetLevel - playerLevel);
		int baseDP = (int) (target.getObjectTemplate().getCreatureStatData().getExp() * 0.04);
		return (int) Math.floor(baseDP * xpPercentage * player.getRates().getGroupXpRate() / 100);
	}

	/**
	 * Hate based on BOOST_HATE stat
	 * Now used only from skills, probably need to use for regular attack
	 * 
	 * @param creature
	 * @param value
	 * @return
	 */
	public static int calculateHate(Creature creature, int value)
	{
		return Math.round(creature.getGameStats().getStat(StatEnum.BOOST_HATE, value).getCurrent() / 100f);
	}

	/**
	 * 
	 * @param player
	 * @param target
	 * @return Damage made to target (-hp value)
	 */
	public static int calculateBaseDamageToTarget(Creature attacker, Creature target, boolean isMainHand)
	{
		return calculatePhysicDamageToTarget(attacker, target, null, 0, isMainHand);
	}

	/**
	 * @param attacker
	 * @param target
	 * @return
	 */
	public static int calculateMagicDamageToTarget(Creature attacker, Creature target)
	{

		CreatureGameStats<?> tgs = target.getGameStats();

		float resultDamage = 0;
		SkillElement element = null;
		if (attacker instanceof Player)
		{
			Equipment equipment = ((Player) attacker).getEquipment();
			Item weapon = equipment.getMainHandWeapon();

			if (weapon != null)
			{
				WeaponStats weaponStat = weapon.getItemTemplate().getWeaponStats();
				if (weaponStat == null)
					return 0;
				int totalMin = weaponStat.getMinDamage();
				int totalMax = weaponStat.getMaxDamage();
				if (totalMax - totalMin < 1)
				{
					log.warn("Weapon stat MIN_MAX_DAMAGE resulted average zero in main-hand calculation");
					log.warn("Weapon ID: " + String.valueOf(equipment.getMainHandWeapon().getItemTemplate().getTemplateId()));
					log.warn("MIN_DAMAGE = " + String.valueOf(totalMin));
					log.warn("MAX_DAMAGE = " + String.valueOf(totalMax));
				}

				resultDamage = Rnd.get(totalMin, totalMax);
				//TODO move to controller
				if (weapon.getItemTemplate().getWeaponType() == WeaponType.BOW)
					equipment.useArrow();

			}
			else
			//if hand attack
			{
				resultDamage = Rnd.get(16, 20);
			}

			if (attacker.isInState(CreatureState.POWERSHARD))
			{
				Item powerShard = equipment.getMainHandPowerShard();

				if (powerShard != null)
				{
					resultDamage += powerShard.getItemTemplate().getWeaponBoost();

					equipment.usePowerShard(powerShard, 1);
				}
			}
			if (weapon.getItemTemplate().getAttackType() == EAttackType.MAGICAL_FIRE)
				element = SkillElement.FIRE;
			else
				element = SkillElement.WATER;
		}
		resultDamage = resultDamage * (attacker.getGameStats().getKnowledge().getCurrent()) * 0.01f;
		resultDamage = resultDamage / (target.getGameStats().getKnowledge().getCurrent() / 100f);
		resultDamage = attacker.getGameStats().getStat(StatEnum.MAGICAL_ATTACK, resultDamage).getCurrent();

		if (element != null)
			resultDamage = Math.round(resultDamage * (1 - tgs.getMagicalDefenseFor(element) / 1000f));
		resultDamage = adjustDamages(attacker, target, resultDamage);
		if (resultDamage <= 0)
			resultDamage = 1;

		return Math.round(resultDamage);
	}

	/**
	 * 
	 * @param player
	 * @param target
	 * @param skillDamages
	 * @return Damage made to target (-hp value)
	 */
	public static int calculatePhysicDamageToTarget(Creature attacker, Creature target, EffectTemplate effectTemplate, int skillDamages, boolean isMainHand)
	{
		CreatureGameStats<?> tgs = target.getGameStats();

		float resultDamage = 0;
		if (attacker instanceof Player)
		{
			Equipment equipment = ((Player) attacker).getEquipment();
			Item weapon;
			if (isMainHand)
				weapon = equipment.getMainHandWeapon();
			else
				weapon = equipment.getOffHandWeapon();

			if (weapon != null)
			{
				WeaponStats weaponStat = weapon.getItemTemplate().getWeaponStats();
				if (weaponStat == null)
					return 0;
				int totalMin = weaponStat.getMinDamage();
				int totalMax = weaponStat.getMaxDamage();
				if (totalMax - totalMin < 1)
				{
					log.warn("Weapon stat MIN_MAX_DAMAGE resulted average zero in main-hand calculation");
					log.warn("Weapon ID: " + String.valueOf(equipment.getMainHandWeapon().getItemTemplate().getTemplateId()));
					log.warn("MIN_DAMAGE = " + String.valueOf(totalMin));
					log.warn("MAX_DAMAGE = " + String.valueOf(totalMax));
				}

				resultDamage = Rnd.get(totalMin, totalMax);
				//TODO move to controller
				if (weapon.getItemTemplate().getWeaponType() == WeaponType.BOW)
					equipment.useArrow();

			}
			else
			//if hand attack
			{
				resultDamage = Rnd.get(16, 20);
			}
			if (attacker.isInState(CreatureState.POWERSHARD))
			{
				Item powerShard;
				if (isMainHand)
					powerShard = equipment.getMainHandPowerShard();
				else
					powerShard = equipment.getOffHandPowerShard();

				if (powerShard != null)
				{
					resultDamage += powerShard.getItemTemplate().getWeaponBoost();

					equipment.usePowerShard(powerShard, 1);
				}
			}
		}
		else
		{
			resultDamage = attacker.getGameStats().getMainHandPAttack().getCurrent();
			int rnd = (int) (resultDamage * 0.25);
			resultDamage += Rnd.get(-rnd, rnd);
		}

		if (effectTemplate != null && effectTemplate instanceof DamageEffect)
		{
			DamageEffect skillAttackTemplate = (DamageEffect) effectTemplate;
			switch (skillAttackTemplate.getMode())
			{
				case ADD:
					resultDamage += skillDamages;
					break;
				case PERCENT:
					resultDamage = resultDamage * (skillDamages + 100) / 100;
			}
		}

		//adjusting baseDamages according to attacker and target level
		//
		resultDamage = resultDamage * (attacker.getGameStats().getPower().getCurrent()) * 0.01f;
		resultDamage = resultDamage / (target.getGameStats().getPower().getCurrent() / 100f);
		resultDamage = attacker.getGameStats().getStat(StatEnum.PHYSICAL_ATTACK, resultDamage).getCurrent();
		resultDamage -= (tgs.getPDef().getCurrent() * 0.10f);
		resultDamage = adjustDamages(attacker, target, resultDamage);
		if (resultDamage <= 0)
			resultDamage = 1;

		return Math.round(resultDamage);
	}

	/**
	 * @param player
	 * @param target
	 * @param skillEffectTemplate
	 * @return HP damage to target
	 */
	public static int calculateMagicDamageToTarget(Creature speller, Creature target, int baseDamages, SkillElement element)
	{
		CreatureGameStats<?> sgs = speller.getGameStats();
		CreatureGameStats<?> tgs = target.getGameStats();

		float totalBoostMagicalSkill = sgs.getMagicalBoost().getCurrent();

		float damages = (baseDamages * sgs.getKnowledge().getCurrent() / 100f) + (totalBoostMagicalSkill / 5);
		damages = Math.round((damages * 100f / tgs.getKnowledge().getCurrent()));
		//adjusting baseDamages according to attacker and target level
		//
		damages = adjustDamages(speller, target, damages);

		// element resist: fire, wind, water, eath
		//
		// 10 elemental resist ~ 1% reduce of magical baseDamages
		//
		damages = Math.round(damages * (1 - tgs.getMagicalDefenseFor(element) / 1000f));

		// IMPORTANT NOTES
		//
		// magicalResistance supposed to be counted to EVADE magic, not to reduce damage, only the elementaryDefense it's counted to reduce magic attack
		//
		//     so example if 200 magic resist vs 100 magic accuracy, 200 - 100 = 100/10 = 0.10 or 10% chance of EVADE
		//
		// damages -= Math.round((elementaryDefense+magicalResistance)*0.60f);

		if (damages <= 0)
		{
			damages = 1;
		}

		return Math.round(damages);
	}

	/**
	 * adjust baseDamages according to their level || is PVP?
	 *
	 * @ref:
	 *
	 * @param attacker lvl
	 * @param target lvl
	 * @param baseDamages
	 *
	 **/
	public static float adjustDamages(Creature attacker, Creature target, float Damages)
	{

		if (attacker instanceof Npc && ((CreatureTemplate)attacker.getObjectTemplate()).getCommonData().getRank().ordinal() < 3)
		{
			int attackerLevel = attacker.getLevel();
			int targetLevel = target.getLevel();
			float multipler = 1.0f;
			int differ = (targetLevel - attackerLevel);
			switch (differ)
			{
				case -9:
				case -8:
				case -7:
				case -6:
				case -5:
				case -4:
				case -3:
					multipler = 1.045f;
					break;
				case -2:
					multipler = 1.033f;
					break;
				case -1:
				case 0:
				case 1:
				case 2:
					break;
				case 3:
					multipler = 0.88f;
					break;
				case 4:
					multipler = 0.82f;
					break;
				case 5:
					multipler = 0.72f;
					break;
				case 6:
					multipler = 0.62f;
					break;
				case 7:
					multipler = 0.52f;
					break;
				case 8:
					multipler = 0.42f;
					break;
				case 9:
					multipler = 0.32f;
					break;
				case 10:
					multipler = 0.22f;
					break;

				default:
					if (differ > 10)
						multipler = 0.11f;
			}
			Damages *= multipler;
		}
		//PVP damages is capped of 60% of the actual baseDamage
		if ((attacker instanceof Player) && (target instanceof Player))
		{
			Damages = Math.round(Damages * 0.60f);
			float pvpAttackBonus = attacker.getGameStats().getStat(StatEnum.PVP_ATTACK_RATIO, 0).getCurrent() * 0.001f;
			float pvpDefenceBonus = target.getGameStats().getStat(StatEnum.PVP_DEFEND_RATIO, 0).getCurrent() * 0.001f;
			Damages = Math.round(Damages + (Damages * pvpAttackBonus) - (Damages * pvpDefenceBonus));
			return Damages;
		}

		return Damages;

	}

	/**
	 *  Calculates DODGE chance
	 *  
	 * @param attacker
	 * @param attacked
	 * @return int
	 */
	public static float calculatePhysicalDodgeRate(Creature attacker, Creature attacked)
	{
		//check always dodge
		if (attacked.getObserveController().checkAttackStatus(AttackStatus.DODGE))
			return 100;

		float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent();

		float dodgeRate = (attacked.getGameStats().getEvasion().getCurrent() - accuracy) / 10;
		// maximal dodge rate
		if (dodgeRate > 30)
			dodgeRate = 30;

		if (dodgeRate <= 0)
			return 1;

		return dodgeRate;
	}

	/**
	 *  Calculates PARRY chance
	 *  
	 * @param attacker
	 * @param attacked
	 * @return int
	 */
	public static float calculatePhysicalParryRate(Creature attacker, Creature attacked)
	{
		//check always parry
		if (attacked.getObserveController().checkAttackStatus(AttackStatus.PARRY))
			return 100;

		float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent();

		float parryRate = (attacked.getGameStats().getParry().getCurrent() - accuracy) / 10;
		// maximal parry rate
		if (parryRate > 40)
			parryRate = 40;

		if (parryRate <= 0)
			return 1;

		return parryRate;
	}

	/**
	 *  Calculates BLOCK chance
	 *  
	 * @param attacker
	 * @param attacked
	 * @return int
	 */
	public static float calculatePhysicalBlockRate(Creature attacker, Creature attacked)
	{
		//check always block
		if (attacked.getObserveController().checkAttackStatus(AttackStatus.BLOCK))
			return 100;

		float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent();

		float blockRate = (attacked.getGameStats().getBlock().getCurrent() - accuracy) / 10;
		// maximal block rate
		if (blockRate > 50)
			blockRate = 50;

		if (blockRate <= 0)
			return 1;

		return blockRate;
	}

	/**
	 *  Calculates CRITICAL chance
	 *  
	 * @param attacker
	 * @return double
	 */
	public static double calculatePhysicalCriticalRate(Creature attacker, Creature attacked, boolean isMainHand)
	{
		float critical = 0;
		if (!isMainHand && attacker instanceof Player)
			critical = ((PlayerGameStats) attacker.getGameStats()).getOffHandPCritical().getCurrent();
		else
			critical = attacker.getGameStats().getMainHandPCritical().getCurrent();

		critical = attacked.getGameStats().getStat(StatEnum.CRITICAL_RESIST, critical).getCurrent();
		double criticalRate;

		if (critical <= 440)
			criticalRate = critical * 0.1f;
		else if (critical <= 600)
			criticalRate = (440 * 0.1f) + ((critical - 440) * 0.05f);
		else
			criticalRate = (440 * 0.1f) + (160 * 0.05f) + ((critical - 600) * 0.02f);
		// minimal critical rate
		if (criticalRate < 1)
			criticalRate = 1;

		return criticalRate;
	}

	/**
	 *  Calculates RESIST chance
	 *  
	 * @param attacker
	 * @param attacked
	 * @return int
	 */
	public static int calculateMagicalResistRate(Creature attacker, Creature attacked)
	{
		if (attacked.getObserveController().checkAttackStatus(AttackStatus.RESIST))
			return 100;

		int resistRate = Math.round((attacked.getGameStats().getMagicalResist().getCurrent() - attacker.getGameStats().getMagicalAccuracy().getCurrent()) / 10);

		return resistRate;
	}

	/**
	 * Calculates the fall damage
	 * 
	 * @param player
	 * @param distance
	 * @return True if the player is forced to his bind location.
	 */
	public static boolean calculateFallDamage(Player player, float distance)
	{
		if (player.isInvul())
		{
			return false;
		}

		if (distance >= FallDamageConfig.MAXIMUM_DISTANCE_DAMAGE)
		{
			player.getFlyController().onStopGliding();
			player.onDie(player);

			player.getReviveController().bindRevive();
			return true;
		}
		else if (distance >= FallDamageConfig.MINIMUM_DISTANCE_DAMAGE)
		{
			float dmgPerMeter = player.getLifeStats().getMaxHp() * FallDamageConfig.FALL_DAMAGE_PERCENTAGE / 100f;
			int damage = (int) (distance * dmgPerMeter);

			player.getLifeStats().reduceHp(damage, player);
			PacketSendUtility.sendPacket(player, new SM_ATTACK_STATUS(player, SM_ATTACK_STATUS.TYPE.DAMAGE, 0, damage));
		}

		return false;
	}

}
